#!/usr/bin/python
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

'''Test utility for verifying ChromeOS firmware.'''

import datetime
import functools
import getopt
import os
import re
import shutil
import sys
import tempfile
import traceback

import cgpt_handler
import cgpt_state
import chromeos_interface
import flashrom_handler
import kernel_handler
import saft_flashrom_util
import tpm_handler

#
# We need to know the names of two files:
#
# - the SAFT shell script used by the upstart script (/etc/init/saft.conf) to
#   determine if SAFT is in progress
#
# - the name of the log file where the upstart script redirects its output.
#
# These file names are defined in the upstart script as <NAME>=<VALUE>, one
# per line, using names below:

# SAFT shell script
SAFT_SCRIPT_VAR_NAME = 'SAFT_SCRIPT'

# Upstart log file
CONF_LOG_VAR_NAME = 'SAFT_LOG'

# This is a template used to populate the SAFT shell script which
# /etc/init/saft.conf is looking for.
SAFT_SCRIPT_TEMPLATE = '''
#!/bin/sh
# Copyright (c) 2010 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

# Do not edit, this file was autogenerated.

# The only parameter is the name of the Flash block device. The root main root
# file system is presumed to be /dev/<flash_block_dev>3.
#
# TODO(vbendeb): instead of using the hardcoded device name look for block
#                labeled  C-KEYFOB

set -e

saft_dev="/dev/${1}3"
root_dev=$(rootdev -s)

if [ "${saft_dev}" = "${root_dev}" ]; then
    mount_point=""
else
    mount_point='/tmp/saft_device'
    mkdir "${mount_point}"
    mount "${saft_dev}" "${mount_point}"
fi

cd "${mount_point}%s"

# Start our own X server to make it easier to control the screen
X_PORT=1
X :"${X_PORT}" -config saft.xorg.conf vt02 > /dev/null 2>&1 &
xpid="$!"
initctl stop update-engine  # Prevent messing with cgpt attributes
export DISPLAY=":${X_PORT}.0"
./%s --next_step
kill "${xpid}"
'''

# Subdirectory to keep the state of this test over reboots. Created in the
# /var on the USB flash drive the test is running from.
STATE_SUBDIR = '.fw_test'

# Files storing SAFT state over reboots, located in state_dir defined below.
LOG_FILE = 'fw_test_log.txt'  # Test log.
STEP_FILE = 'fw_test_step'  # The next test step number.
FW_BACKUP_FILE = 'flashrom.bak'  # Preserved original flashrom contents.
FW_COPY_FILE = 'flashrom.new'  # A copy of the flashrom contents being tested.
FWID_BACKUP_FILE = 'fwid.bak'  # FWID reported by the original firmware.
FWID_NEW_FILE = 'fwid.new'  # FWID reported by the firmware being tested.

# The list of shell executables necessary for this program to work.
REQUIRED_PROGRAMS = '''
cgpt crossystem blkid flashrom readlink rootdev vbutil_firmware vbutil_kernel
mosys
'''


FLASHROM_HANDLER = flashrom_handler.FlashromHandler()
CHROS_IF = chromeos_interface.ChromeOSInterface(__name__ != '__main__')
BASE_STORAGE_DEVICE = CHROS_IF.get_root_dev()
cgpt_st = cgpt_state.CgptState('COMPLETE', CHROS_IF, BASE_STORAGE_DEVICE)

def allow_multiple_section_input(image_operator):
    @functools.wraps(image_operator)
    def wrapper(self, section):
        if type(section) is not tuple:
            image_operator(self, section)
        else:
            for sec in section:
                image_operator(self, sec)
    return wrapper

class FwError(Exception):
    '''A class to encapsulate this module specific exceptions.'''
    pass


def RetriveSaftConfDefinion(name):
    '''Find a name definition in the SAFT upstart script.

    This function scans the /etc/init/saft.conf for <name>=<value> lines. and
    if found - returns the <value> string.

    It will throw an IOError exception in case the upstart script is not
    present or FwError() in case <name> definition is not in the file.
    '''
    pattern = name + '='
    conf_file = '/etc/init/saft.conf'

    for line in open(conf_file, 'r').readlines():
        line = line.strip()
        if not line.startswith(pattern):
            continue
        pieces = line.split('=')
        if len(pieces) != 2 or pieces[0] != name:
            continue
        return pieces[1].strip('\'"')

    # If we did not find it - something is very wrong.
    raise FwError('name %s not found in %s' % (name, conf_file))


class FirmwareTest(object):
    '''An object to represent a Firmware Semi Automated test (SAFT).

    It follows the steps as enumerated in test_state_sequence below,
    restarting the target after each step. The first time around the
    init_fw_test() method must be called to set up the persistent environment.
    All following invocations should initialize the object (using .init()) and
    the invoke the next_step() method.
    '''

    def __init__(self):
        '''Object initializer, does nothing to make mocking easier.'''
        self.mydir = None
        self.base_partition = None
        self.chros_if = None
        self.progname = None
        self.test_state_sequence = None
        self.kern_handler = None
        self.step_failed = False
        self.window = None
        self.tpm_handler = None
        self.root_dev = None

    def _verify_fw_id(self, compare_to_file):
        '''Verify if the current firmware ID matches the contents a file.

        compare_to_file - a string, name of the file in the state directory.
        '''
        old_fwid = open(
            self.chros_if.state_dir_file(compare_to_file), 'r').read()
        now_fwid = self.chros_if.cs.fwid
        return old_fwid == now_fwid

    def _get_step(self):
        '''Set the current value of SAFT step number.'''
        step_file = self.chros_if.state_dir_file(STEP_FILE)
        step = open(step_file, 'r').read().strip()
        return int(step)

    def _set_step(self, step):
        '''Set the SAFT step number to control the next test pass.'''
        step_file = self.chros_if.state_dir_file(STEP_FILE)
        open(step_file, 'w').write('%d' % step)

    def _handle_saft_script(self, install):
        '''Install or remove the SAFT shell script.

      On SAFT initialization, this function is invoked to install the shell
      script which the upstart script /etc/init/saft.conf will be passing
      control to after each restart. The presence of the shell script is an
      indication that SAFT is in progress.

      The shell script is expected to be installed in the stateful partition
      of the flash drive. When invoked, the shell script completes setting up
      the environment and in turn invokes this program passing it --next_step
      as the command line parameter.

      When SAFT finishes all steps, this function is called to remove the
      shell script so that next time the traget restarts it does not try to
      execute SAFT anymore.
      '''

        if not self.chros_if.target_hosted():
            print 'bypassing upstart management'
            return

        label_pattern = re.compile('/dev/(.+): LABEL="C-STATE"')
        tmp_dir = tempfile.mkdtemp()

        # Find out our file name off the root of the device
        mount_point = (self.chros_if.run_shell_command_get_output(
                'df %s' % self.mydir)[-1]).split()[-1]
        this_dir = self.mydir[len(mount_point):]

        for line in self.chros_if.run_shell_command_get_output('blkid'):
            match = label_pattern.search(line)
            if not match:
                continue
            dev = match.groups(0)[0]
            mount_point = self.chros_if.get_writeable_mount_point(
                '/dev/' + dev, tmp_dir)
            script_name = RetriveSaftConfDefinion(SAFT_SCRIPT_VAR_NAME)

            # Find location of the script in the currently mounted file
            # system.
            file_name = os.path.join(mount_point, script_name.lstrip('/'))
            self.chros_if.log('Handling ' + file_name)
            if install:
                saft_script = open(file_name, 'w')
                saft_script.write(SAFT_SCRIPT_TEMPLATE % (
                        this_dir, self.progname))
                saft_script.close()
                os.chmod(file_name, 0700)
            else:
                os.unlink(file_name)
            if mount_point.startswith(tmp_dir):
                self.chros_if.run_shell_command('umount %s' % tmp_dir)

        os.rmdir(tmp_dir)

    def _check_runtime_env(self):
        '''Ensure that the script is running in proper environment.

      This involves checking that the script is running off a removable
      device, configuring proper file names for logging, etc.
      '''
        line = self.chros_if.run_shell_command_get_output(
            'df %s' % self.mydir)[1]

        self.base_partition = line.split()[0]
        if self.base_partition == '/dev/root':
            self.base_partition = self.chros_if.get_root_part()

        if not self.chros_if.is_removable_device(self.base_partition):
            raise FwError(
                'This test must run off a removable device, not %s'
                % self.base_partition)

        env_root = '/var'
        state_fs = '%s1' % self.base_partition[:-1]

      # is state file system mounted?
        for line in self.chros_if.run_shell_command_get_output('mount'):
            if line.startswith('%s on ' % state_fs):
                mount_point = line.split()[2]
                state_root = mount_point + env_root
                break
        else:
            tmp_dir = tempfile.mkdtemp()
            self.chros_if.run_shell_command('mount %s %s' % (state_fs, tmp_dir))
            state_root = '%s%s' % (tmp_dir, env_root)

        self.chros_if.init(os.path.join(state_root, STATE_SUBDIR), LOG_FILE)


    def init(self, progname, chros_if,
             kern_handler=None, state_sequence=None, tpm_handler=None):
        '''Initialize the Firmware self test instance.

        progname - a string, name of this program as it was invoked.

        chros_if - an object of ChromeOSInterface type to be initialized and
                   used by this instance of FirmwareTest.

        kern_handler - an object providing SAFT with services manipulating
                   kernel images. Unittest does not provide this object.

        test_state_sequence - a tuple of three-tuples driving test execution,
                   see description below. Unittest does not provide this
                   sequence.
        '''
        real_name = os.path.realpath(progname)
        self.mydir = os.path.dirname(real_name)
        self.progname = os.path.basename(real_name)
        self.chros_if = chros_if
        self._check_runtime_env()
        self.test_state_sequence = state_sequence
        if kern_handler:
            self.kern_handler = kern_handler
            self.kern_handler.init(self.chros_if)
        if tpm_handler:
            self.tpm_handler = tpm_handler
            self.tpm_handler.init(self.chros_if)

        if self.tpm_handler:
            if not self.chros_if.is_removable_device(self.base_partition):
                # On each non USB flash rooted start confirm that kernel and
                # BIOS versions and TPM setting match.
                vers_a = FLASHROM_HANDLER.get_version('a')
                vers_b = FLASHROM_HANDLER.get_version('b')
                if not self.tpm_handler.fw_version_good(vers_a, vers_b):
                    raise FwError('TPM firmware version mismatch (%d %d %s)' % (
                            vers_a, vers_b, self.base_partition))
                if self.kern_handler:
                    vers_a = self.kern_handler.get_version('a')
                    vers_b = self.kern_handler.get_version('b')
                    if not self.tpm_handler.kernel_version_good(vers_a, vers_b):
                        raise FwError('TPM kernel version mismatch (%d %d %s)' % (
                                vers_a, vers_b, self.base_partition))

    def set_try_fw_b(self):
        '''Request running firmware B on the next restart.'''
        self.chros_if.log('Requesting restart with FW B')
        self.chros_if.cs.fwb_tries = 1

    def request_recovery_boot(self):
        '''Request running in recovery mode on the restart.'''
        self.chros_if.log('Requesting restart in recovery mode')
        self.chros_if.cs.request_recovery()


    @allow_multiple_section_input
    def restore_firmware(self, section):
        '''Restore the previously corrupted firmware section signature.'''
        self.chros_if.log('Restoring firmware %s' % section)
        FLASHROM_HANDLER.restore_firmware(section)

    @allow_multiple_section_input
    def corrupt_firmware(self, section):
        '''Corrupt the requested firmware section signature.'''
        self.chros_if.log('Corrupting firmware %s' % section)
        FLASHROM_HANDLER.corrupt_firmware(section)

    @allow_multiple_section_input
    def corrupt_kernel(self, section):
        '''Corrupt the requested kernel section.'''
        self.chros_if.log('Corrupting kernel %s' % section)
        self.kern_handler.corrupt_kernel(section)

    @allow_multiple_section_input
    def restore_kernel(self, section):
        '''restore the requested kernel section.'''
        self.chros_if.log('restoring kernel %s' % section)
        self.kern_handler.restore_kernel(section)

    def cgpt_test_start(self):
        '''Set up CGPT test sequence.'''
        cgpth = cgpt_handler.CgptHandler(self.chros_if)
        cgpth.read_device_info(BASE_STORAGE_DEVICE)

        # Set gpt attributes for kernels A and B such that after reset it comes
        # back up using kernel A, with tries A set to 14 (decremented) and
        # other A and B attributes unchanged.
        properties = {'successful': 0,
                      'tries': 15,
                      'priority': 10,
                      }
        cgpth.set_partition(BASE_STORAGE_DEVICE, 'KERN-A', properties)
        properties['priority'] = 9
        cgpth.set_partition(BASE_STORAGE_DEVICE, 'KERN-B', properties)

    def cgpt_test_1(self):
        '''CGPT test checking the attributes settings by the firmware.'''
        expected_cgpt = {
            'KERN-A' : {'priority': 10, 'tries': 14, 'successful': 0},
            'KERN-B' : {'priority': 9, 'tries': 15, 'successful': 0}
            }
        cgpth = cgpt_handler.CgptHandler(self.chros_if)
        cgpth.read_device_info(BASE_STORAGE_DEVICE)
        for part, state in expected_cgpt.iteritems():
            props = cgpth.get_partition(BASE_STORAGE_DEVICE, part)
            for prop, value in state.iteritems():
                if value != props[prop]:
                    self.chros_if.log('wrong partition %s value' % part)
                    self.chros_if.log(cgpth.dump_partition(BASE_STORAGE_DEVICE,
                                                           part))
                    self.step_failed = True
                    break

    def move_kernel_backward(self, section):
        '''Decrement kernel version for the requested section.'''
        new_version = self.kern_handler.get_version(section) - 1
        self.chros_if.log(
            'setting kernel section %s version to %d' % (section, new_version))
        self.kern_handler.set_version(section, new_version)

    def move_firmware_backward(self, section):
        '''Decrement firmware version for the requested section.'''
        new_version = FLASHROM_HANDLER.get_section_version(section) - 1
        flags = FLASHROM_HANDLER.get_section_flags(section)
        self.chros_if.log(
            'setting firmware section %s version to %d' % (
                section, new_version))
        FLASHROM_HANDLER.set_section_version(section, new_version, flags,
                                             write_through=True)

    def jump_kernels_forward(self):
        '''Add two to both kernels' versions.

        This compensates for the previous decrement and increases the version
        number by one as compared to the original state (at the start of the
        test).
        '''
        for section in ('a', 'b'):
            new_version = self.kern_handler.get_version(section) + 2
            self.chros_if.log(
                'setting section %s version to %d' % (section, new_version))
            self.kern_handler.set_version(section, new_version)

    def jump_firmwares_forward(self):
        '''Add two to both firmwares' versions.

        This compensates for the previous decrement and increases the version
        number by one as compared to the original state (at the start of the
        test).
        '''
        for section in ('a', 'b'):
            new_version = FLASHROM_HANDLER.get_section_version(section) + 2
            flags = FLASHROM_HANDLER.get_section_flags(section)
            self.chros_if.log(
                'setting firmware section %s version to %d' % (
                    section, new_version))
            FLASHROM_HANDLER.set_section_version(section, new_version, flags,
                                                 write_through=True)

    def terminate_tpm_tests(self):
        '''Restore TPM and kernel states.

        Move kernel and firmware versions to where they were before SAFT
        started. Set the TPM's values of kernel and firmware versions to the
        lowest of the two kernels/firmwares available on the system. Prevent
        future TPM write accesses by restoring the upstart file.
        '''
        new_tpm_kern_version  = 0xffff
        new_tpm_fw_version  = 0xffff
        for section in ('a', 'b'):
            new_version = self.kern_handler.get_version(section) - 1
            self.chros_if.log(
                'setting kernel section %s version to %d' % (
                    section, new_version))
            self.kern_handler.set_version(section, new_version)
            new_tpm_kern_version = min(new_version, new_tpm_kern_version)

            new_version = FLASHROM_HANDLER.get_section_version(section)  - 1
            flags = FLASHROM_HANDLER.get_section_flags(section)
            self.chros_if.log(
                'setting firmware section %s version to %d' % (
                    section, new_version))
            FLASHROM_HANDLER.set_section_version(section, new_version, flags,
                                                 write_throught=True)
            new_tpm_fw_version = min(new_version, new_tpm_fw_version)

        self.tpm_handler.set_kernel_version(new_tpm_kern_version)
        self.tpm_handler.set_fw_version(new_tpm_fw_version)
        self.tpm_handler.disable_write_access()

    def revert_firmware(self):
        '''Restore firmware to the image backed up when SAFT started.'''
        self.chros_if.log('restoring original firmware image')
        self.chros_if.run_shell_command(
            'flashrom -w %s' % self.chros_if.state_dir_file(FW_BACKUP_FILE))

    def new_fw_image(self, image_file = None):
        FLASHROM_HANDLER.new_image(image_file)
        FLASHROM_HANDLER.verify_image()
        if self.tpm_handler:
            vers_a = FLASHROM_HANDLER.get_section_version('a')
            vers_b = FLASHROM_HANDLER.get_section_version('b')
            if not self.tpm_handler.fw_version_good(vers_a, vers_b):
                raise FwError('TPM firmware version mismatch')

    def prepare_tpm_tests(self):
        '''Prepare TPM for testing.

        Enable write access on the next reboot, and roll back the kernel we
        are running now (the other kernel is expected to be used on the next
        restart).
        '''
        mount_point = self.chros_if.run_shell_command_get_output(
            'df %s' % self.mydir)[-1].split()[-1]
        cfg_file = os.path.join(mount_point, 'etc/init/tcsd.conf')
        self.tpm_handler.enable_write_access(cfg_file)
        self.move_kernel_backward('a')

    def init_fw_test(self, opt_dictionary, chros_if):
        '''Prepare firmware test context.

      This function tries creating the state directory for the fw test and
      initializes the test state machine.

      Return
        True on success
        False on any failure or if the directory already exists
      '''
        chros_if.init_environment()
        chros_if.log('Automated firmware test log generated on %s' % (
                datetime.datetime.strftime(
                    datetime.datetime.now(), '%b %d %Y')))
        chros_if.log('Original boot state %s' % chros_if.boot_state_vector())
        self.chros_if = chros_if
        fw_image = opt_dictionary['image_file']
        self.new_fw_image()
        FLASHROM_HANDLER.dump_whole(
            self.chros_if.state_dir_file(FW_BACKUP_FILE))
        self.new_fw_image(fw_image)
        self._handle_saft_script(True)
        open(self.chros_if.state_dir_file(FWID_BACKUP_FILE), 'w'
             ).write(self.chros_if.cs.fwid)
        shutil.copyfile(fw_image, self.chros_if.state_dir_file(FW_COPY_FILE))

        self._set_step(0)
        cgpt_st.set_step(0)

    def next_step(self):
        '''Function to execute a single SAFT step.

      This function is running after each reboot. It determines the current
      step the SAFT is on, executes the appropriate action, increments the
      step value and then restats the machine.
      '''

        this_step = self._get_step()
        self.chros_if.log('\nStarting step %d' % this_step)

        # Import when needed, because otherwise running test generates a
        # warning exception about the unavailable display.
        window = __import__('window')
        self.window = window.GraphThread()

        if this_step == 0:
            open(self.chros_if.state_dir_file(FWID_NEW_FILE), 'w'
                 ).write(self.chros_if.cs.fwid)

            if self._verify_fw_id(FWID_BACKUP_FILE):
          # we expected FWID to change, but it did not - have the firmware
          # been even replaced?
                self.chros_if.log('New firmware - old FWID')
                self.finish_saft(False)
        test_state_tuple = self.test_state_sequence[this_step]
        expected_vector = test_state_tuple[0]
        action = test_state_tuple[1]

        # Display cgpt sub step if the action is a cgpt test
        step_display = "SAFT's performing step #%d"
        step_tuple = (this_step,)
        if action == cgpt_st.test_loop:
            step_display += " (cgpt test step #%d)"
            step_tuple += (cgpt_st.get_step(),)
        self.window.display_text(step_display % step_tuple)

        boot_vector = self.chros_if.boot_state_vector()
        self.chros_if.log('Rebooted into state %s' % boot_vector)

        conf_log_file = RetriveSaftConfDefinion(CONF_LOG_VAR_NAME)

        if os.path.exists(conf_log_file):
            contents = open(conf_log_file).read().rstrip()
            if len(contents):
                self.chros_if.log('startup log contents:')
                self.chros_if.log(contents)

        if not self.chros_if.cmp_boot_vector(boot_vector, expected_vector):
            self.chros_if.log('Error: Wrong boot vector, %s was expected'
                % expected_vector)
            self.finish_saft(False)
        if this_step == (len(self.test_state_sequence) - 1):
            self.finish_saft(True)
        if action and not self._verify_fw_id(FWID_NEW_FILE):
            self.chros_if.log('Error: Wrong FWID value')
            self.finish_saft(False)

        inc_step = 1
        if action:
            FLASHROM_HANDLER.new_image()
            if action == cgpt_st.test_loop:
                inc_step = action()
            elif len(test_state_tuple) > 2:
                self.chros_if.log('calling %s with parameter %s' % (
                        str(action), str(test_state_tuple[2])))
                action(test_state_tuple[2])
            else:
                self.chros_if.log('calling %s' % str(action))
                action()
            if self.step_failed:
                self.finish_saft(False)

        self._set_step(this_step + inc_step)
        self.chros_if.run_shell_command('reboot')

    def finish_saft(self, success):
        '''SAFT completed, restore the environment and expose the results.

        Delete the startup script from the removable device, copy the log to
        '/var' directory on the current drive and exit.

        Input:
          success - a Boolean set to True if the test has passed through all
            steps. Affects placing of the last line 'we are done' in the log.
        '''

        if success and not self._verify_fw_id(FWID_BACKUP_FILE):
            self.chros_if.log(
                'Error: Failed to restore to original firmware')
            success = False
        self.chros_if.log('Removing upstart scripts')
        self._handle_saft_script(False)
        if success:
            self.chros_if.log('we are done!')
            # Have chros_if.shutdown move the log into the '/var' directory to
            # make it easier to see SAFT results.
            self.chros_if.shut_down(os.path.join('/var', LOG_FILE))

        self.window.stop()
        sys.exit(0)

# Firmware self test instance controlling this module.
FST = FirmwareTest()

# This is a tuple of tuples controlling the SAFT state machine. The states are
# expected to be passed strictly in order. The states used to be identified by
# the contents of BINF.[012] files in the sys fs ACPI directory. Now they are
# derived from the crossystem output to match previously reported states.
#
# The first element of each component tuple is the expected state of the
# machine.
#
# The second element of the component tuples is the action to take to
# advance the test. The action is a function to call. The last line has
# action set to None, which indicates to the state machine that the test
# is over.
#
# The third component, if present, is the parameter to pass to the action
# function.

TEST_STATE_SEQUENCE = (
    ('1:1:1:0:3', FST.set_try_fw_b),                   # Step 0
    ('1:2:1:0:3', None),
    ('1:1:1:0:3', FST.corrupt_firmware, 'a'),
    ('1:2:1:0:3', FST.restore_firmware, 'a'),
    ('1:1:1:0:3', FST.corrupt_firmware, ('a', 'b')),
    ('5:0:1:1:3', FST.restore_firmware, ('a', 'b')),   # Step 5
    ('1:1:1:0:3', FST.corrupt_kernel, 'a'),
    ('1:1:1:0:5', FST.corrupt_kernel, 'b'),
    ('6:0:1:1:3', FST.restore_kernel, ('a', 'b')),
    ('1:1:1:0:3', FST.request_recovery_boot),
    ('8:0:1:1:3', FST.prepare_tpm_tests),              # Step 10
    ('1:1:1:0:5', FST.move_kernel_backward, 'b'),
    ('6:0:1:1:3', FST.jump_kernels_forward),
    ('1:1:1:0:3', FST.move_firmware_backward, 'a'),
    ('1:2:1:0:3', FST.move_firmware_backward, 'b'),
    ('5:0:1:1:3', FST.jump_firmwares_forward),         # Step 15
    ('1:1:1:0:3', FST.request_recovery_boot),
    ('8:0:1:1:3', FST.terminate_tpm_tests),
    ('*:*:*:*:*', cgpt_st.test_loop),
    ('1:1:1:0:3', FST.revert_firmware),
    ('1:1:1:0:3', None),                               # Step 20
    )


# The string below serves two purposes:
#
# - spell out the usage string for this program
#
# - provide text for parsing to derive command line flags. The text is split
#   to words, then the words which have -- in them are stripped off the
#   leading/traling brackets and dashes and used to prepare inut parameters
#   for getopt. This is the only way to pass the parameters to getopt, this
#   enforces that each accepted parameter is mentioned in the usage() output.
USAGE_STRING = '''
 [--image_file=<firmware_image_file>] [--pub_key=<file>] [--next_step]

  The program can be invoked in two modes.

  When invoked for the first time, most of the parameters are required to set
  up the test context and start it.

  Specifying --next_step means that the program is being invoked by the
  restarted system, all the context is expected to be available. No other
  parameters are required or expected in that case.
'''


def usage(msg='', retv=0):
    '''Print error message (if any), usage string and exit.

    Depending on the passed in return value use stdout (if retv is 0) or
    stderr (if otherwise).
    '''
    progname = os.path.basename(sys.argv[0])
    if retv:
        ofile = sys.stderr
    else:
        ofile = sys.stdout
    if msg:
        print >> ofile, '%s: %s' % (progname, msg)
        CHROS_IF.log(msg)
    print >> ofile, 'usage: %s %s' % (progname, USAGE_STRING.strip())
    sys.exit(retv)


def get_options_set():
    '''Generate the list of command line options accepted by this program.

  This function derives the set of accepted command line options from
  usage_string. The string is split into words, all words starting with -- are
  considered accepted command line parameters. The presence of an '=' sign in
  the word,means that the command line parameter requires a value.

  Returns a list of strings suitable for use by the getopt module.
  '''

    drop_tail = re.compile('=.*$')
    option_set = []
    items = (' '.join(USAGE_STRING.split('\n'))).split(' ')
    for item in items:
        if '--' in item:
            if item.startswith("'"):
                continue
            option = drop_tail.sub('=', item.strip('-[]'))
            if option not in option_set:
                option_set.append(option)
    return option_set



def main(argv):
    '''Process command line options and invoke the proper test entry point.'''
    (opts, params) = getopt.gnu_getopt(argv[1:], '', get_options_set())
    if params:
        raise FwError('unrecognized parameters: %s' % ' '.join(params))

    opt_dictionary = {}
    for (name, value) in opts:
        opt_dictionary[name.lstrip('-')] = value

    FST.init(argv[0], CHROS_IF,
             kernel_handler.KernelHandler(), TEST_STATE_SEQUENCE,
             tpm_handler.TpmHandler())

    FLASHROM_HANDLER.init(saft_flashrom_util, CHROS_IF,
                         opt_dictionary.get('pub_key'))
    if 'next_step' in opt_dictionary:
        if len(opt_dictionary) != 1:
            usage('--next_step (when specified) must be the only parameter', 1)
        try:
            FST.next_step()
        except SystemExit:
            pass  # No real exception, this is just an exit (for whatever
                  # reason), gtk window already closed.
        except:
            # Whatever error that might be, gtk window must be shut.
            if FST.window:
                FST.window.stop()

            # Make sure exception information is saved in the log.
            exc_type, exc_info, exc_trace = sys.exc_info()
            print 'exception type:', str(exc_type)
            print 'exception info:', str(exc_info)
            traceback.print_tb(exc_trace)
        sys.exit(0)

    # check if all executables are available
    missing_execs = []
    for prog in REQUIRED_PROGRAMS.split():
        if not prog:
            continue # ignore line breaks
        if not CHROS_IF.exec_exists(prog):
            missing_execs.append(prog)
    if missing_execs:
        usage('Program(s) %s not found in PATH' % ' '.join(missing_execs), 1)

    if 'image_file' not in opt_dictionary:
        usage(retv=1)

    FST.init_fw_test(opt_dictionary, CHROS_IF)

    if not FLASHROM_HANDLER.firmware_sections_equal():
        # This is a temporary measure needed to address the fact that the
        # development BIOS image is built with 'normal' firmware in section B
        # and 'development' firmware in section A. SAFT operation requires
        # 'normal' flavor in both sections. Note that this fix does not affect
        # the file of the new image, only the flashrom contents.
        CHROS_IF.log('modify firmware A to match B')
        FLASHROM_HANDLER.copy_from_to('b', 'a')
    CHROS_IF.log('program the new image')
    FLASHROM_HANDLER.write_whole()
    CHROS_IF.log('restart')
    CHROS_IF.run_shell_command('reboot')
    return 0


if __name__ == '__main__':
    try:
        main(sys.argv)
    except (getopt.GetoptError, ImportError):
        usage(sys.exc_info()[1], 1)
    except (FwError, flashrom_handler.FlashromHandlerError):
        MSG = 'Error: %s' % str(sys.exc_info()[1])
        print MSG
        CHROS_IF.log(MSG)
        sys.exit(1)

    sys.exit(0)
